import math
import json
import pandas as pd
import pandas_profiling
import numpy as np
import matplotlib.pyplot as plt
import plotly.express as px
import chart_studio.plotly as py
import plotly.graph_objs as go
import plotly.figure_factory as ff
from plotly.colors import n_colors
from plotly.offline import iplot, init_notebook_mode
from plotly.subplots import make_subplots
init_notebook_mode(connected=True)
%run src/utils.py
city = 'sevilla'
month = '201909'
filename_in = 'src/data/' + city + '-' + month + '-listings.csv'
filename_out = 'src/data/' + city + '-' + month + '-listings-CLEAN.csv'
df = pd.read_csv(filename_in, low_memory=False)
df.info()
with open('src/geo/' + city + '.neighbourhoods.geojson') as f:
city_nb = fix_geojson(json.load(f))
En el descarte inicial se desechan las características que se consideran menos útiles para el análisis exploratorio. Tras el análisis exploratorio habrá otro descarte de características que si bien habían sido interesantes para el exploratorio no lo serán para el modelado de la solución.
useless_cols = [
'id',
'listing_url',
'scrape_id',
'last_scraped',
'name',
'summary',
'space',
'description',
'experiences_offered',
'neighborhood_overview',
'notes',
'transit',
'access',
'interaction',
'house_rules',
'thumbnail_url',
'medium_url',
'picture_url',
'xl_picture_url',
'host_id',
'host_url',
'host_name',
'host_since',
'host_location',
'host_about',
'host_response_rate',
'host_acceptance_rate',
'host_is_superhost',
'host_thumbnail_url',
'host_picture_url',
'host_neighbourhood',
'host_listings_count',
'host_total_listings_count',
'host_has_profile_pic',
'host_identity_verified',
'host_verifications',
'street',
'neighbourhood',
'city',
'state',
'zipcode',
'market',
'smart_location',
'country_code',
'country',
'is_location_exact',
'square_feet',
'weekly_price',
'monthly_price',
'beds',
'bed_type',
'minimum_nights',
'maximum_nights',
'minimum_minimum_nights',
'maximum_minimum_nights',
'minimum_maximum_nights',
'maximum_maximum_nights',
'calendar_updated',
'has_availability',
'availability_30',
'availability_60',
'availability_90',
'availability_365',
'calendar_last_scraped',
'requires_license',
'jurisdiction_names',
'is_business_travel_ready',
'require_guest_profile_picture',
'require_guest_phone_verification',
'calculated_host_listings_count',
'calculated_host_listings_count_entire_homes',
'calculated_host_listings_count_private_rooms',
'calculated_host_listings_count_shared_rooms'
]
df.drop(useless_cols, axis=1, inplace=True)
Se unifican los nombres de los distritos de los distintos datasets para permitir join entre varios datasets cuando sea necesario. Para simplificar, se eliminan caracteres especiales como los acentos y se cambia el nombre de la columnas de barrio y distrito.
df['district'] = df['neighbourhood_group_cleansed'].apply(lambda x: remove_accents(x))
df.drop(['neighbourhood_group_cleansed'], axis=1, inplace=True)
df['neighbourhood'] = df['neighbourhood_cleansed']
df['neighbourhood'] = df['neighbourhood'].apply(lambda x: remove_accents(str.upper(x)))
df.drop(['neighbourhood_cleansed'], axis=1, inplace=True)
Se realiza la conversión de formato texto a formato fecha para facilitar posteriores operaciones.
df['first_review'] = pd.to_datetime(df['first_review'], format='%Y-%m-%d')
df['last_review'] = pd.to_datetime(df['last_review'], format='%Y-%m-%d')
Se realiza la conversión de formato texto en dólares a formato numérico en euros.
dollar_to_euro_rate = 0.9
dollar_cols = [
'price',
'security_deposit',
'cleaning_fee',
'extra_people'
]
for col in dollar_cols:
if col in df.columns:
df[col].fillna('$0', inplace=True)
df[col] = df[col].apply(lambda x: clean_price_dollar(x))
df[col] = df[col].astype(float)
df[col] = df[col] * dollar_to_euro_rate
df[col] = df[col].round(2)
Se realiza la conversión de formato texto (t para true, f para false) a formato numérico (0 para false, 1 para true).
bin_cols = [
'host_is_superhost',
'host_identity_verified',
'is_location_exact',
'has_availability',
'instant_bookable'
]
for col in bin_cols:
if col in df.columns:
df[col] = df[col].apply(lambda x: get_bin_value_by_char(x))
df[col] = df[col].astype(int)
Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de equipamientos más relevantes que declara cada vivienda y creando una característica binaria para cada tipo de equipamiento. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.
df['amenities'] = df['amenities'].str.lower()
amenities_dict = {}
def collect_amenities(str):
str = str.replace('{', '').replace('}', '').replace('\"', '').strip()
word_list = str.split(",")
for w in word_list:
w = w.strip()
amenities_dict[w] = amenities_dict.get(w, 0) + 1
df['amenities'].apply(lambda x: collect_amenities(x))
amenities_dict = sorted(amenities_dict.items(), key=lambda x: x[1], reverse=True)
top_amenities = [
'wifi',
'essentials',
'kitchen',
'heating',
'washer',
'hangers',
'tv',
'hair dryer',
'iron',
'shampoo',
'laptop friendly workspace',
'air conditioning',
'hot water',
'elevator',
'refrigerator',
'dishes and silverware',
'microwave',
'bed linens',
'no stairs or steps to enter',
'coffee maker',
'cooking basics',
'family/kid friendly',
'long term stays allowed',
'first aid kit',
'oven',
'stove'
]
for v in top_amenities:
new_col_name = 'has_' + v.replace(' ', '_')
df[new_col_name] = df['amenities'].apply(lambda x: contains_bin_value(v, x))
df[new_col_name] = df[new_col_name].astype(int)
df.drop(['amenities'], axis=1, inplace=True)
Se realiza la conversión de un formato no estructurado extrayendo del texto los tipos de verificaciones más relevantes que declara cada anfitrión y creando una característica binaria para cada tipo de verificación. Esto además simplificará la gestión de variables categóricas en el modelado y facilitará en algunos aspectos la exploración.
"""
df['host_verifications'] = df['host_verifications'].str.lower()
hverif_dict = {}
def collect_host_verifications(str):
str = str.replace('[', '').replace(']', '').replace('\"', '').replace('\'', '').strip()
word_list = str.split(",")
for w in word_list:
w = w.strip()
hverif_dict[w] = hverif_dict.get(w, 0) + 1
df['host_verifications'].apply(lambda x: collect_host_verifications(x))
hverif_dict = sorted(hverif_dict.items(), key=lambda x: x[1], reverse=True)
top_verification_modes = [
'phone',
'email',
'government_id',
'reviews',
'jumio',
'offline_government_id',
'selfie',
'identity_manual',
'facebook',
'work_email',
'google'
]
for v in top_verification_modes:
new_col_name = 'host_verified_by_' + v.replace(' ', '_')
df[new_col_name] = df['host_verifications'].apply(lambda x: contains_bin_value(v, x))
df[new_col_name] = df[new_col_name].astype(int)
df.drop(['host_verifications'], axis=1, inplace=True)
"""
Lo relevante para el estudio es si tiene licencia o no, sin importar el identificador.
df['license'].fillna(0, inplace=True)
df['has_license'] = df['license'].apply(lambda x: 1 if x != 0 else 0)
df.drop(['license'], axis=1, inplace=True)
Se van a generar una serie de características nuevas para enriquecer el estudio descriptivo.
Se trata del período que la vivienda lleva explotándose de forma efectiva y se estima como el tiempo transcurrido en meses desde la primera estancia hasta la última.
df['activity_months'] = (df['last_review'] - df['first_review']) / np.timedelta64(1, 'M')
df['activity_months'].fillna(0, inplace=True)
El precio por noche (price) no es un indicador definitivo de los ingresos por estancia porque el precio de una estancia viene determinada por otros factores adicionales:
Por ello también se puede calcular una variable que estime el coste total de una estancia media tomando como media de número de noches 3 y como número de huéspedes el valor medio entre el mínimo y el máximo de ocupantes de la vivienda.
df['cleaning_fee'].fillna(0, inplace=True)
df['extra_people'].fillna(0, inplace=True)
df['guests_included'].fillna(0, inplace=True)
df['accommodates'].fillna(0, inplace=True)
avg_days = 3
df['income_med_occupation'] = df.apply(
lambda r: calculate_income_med_occupation(
r['price'],
r['cleaning_fee'],
r['accommodates'],
r['extra_people'],
r['guests_included'],
avg_days),
axis=1
)
df['price_med_occupation_per_accommodate'] = df.apply(
lambda r: calculate_price_med_occupation_per_accommodate(
r['price'],
r['cleaning_fee'],
r['accommodates'],
r['extra_people'],
r['guests_included'],
avg_days),
axis=1
)
Se excluyen propiedades como los hoteles que ya de por sí son negocios hosteleros ya que el estudio pretende ceñirse a viviendas.
outliers_idx = df[~df['property_type'].isin(['Apartment', 'House', 'Chalet', 'Condominium', 'Loft'])].index
remove_outliers(df, outliers_idx, debug_col='property_type')
outliers_idx = df[df['room_type'].isin(['Hotel room'])].index
remove_outliers(df, outliers_idx, debug_col='room_type')
Las viviendas sin reviews, ya que no hay ningún indicio para saber si llevan o no tiempo publicadas, se excluyen del estudio.
df['number_of_reviews'].fillna(0, inplace=True)
outliers_idx = df[df['number_of_reviews'] < 2]['number_of_reviews'].index.tolist()
remove_outliers(df, outliers_idx, debug_col='number_of_reviews')
Se aplica arbitrariamente una regla de rango intercuartil (IQR) para excluir precios extremos.
outliers_idx = get_outliers_iqr(df['price_med_occupation_per_accommodate'], 4.5)[0]
remove_outliers(df, outliers_idx, debug_col='price_med_occupation_per_accommodate')
Hay viviendas anunciadas cuyo mínimo de noches por estancia es tan elevado (meses, un año o varios años) que se puede deducir que su objetivo o su intención es un alquier de tipo residencial estable, no vacacional.
Se excluyen de forma arbitraria los pisos cuyo número mínimo de noches supere las 70 por considerarlo un alquiler no vacacional.
outliers_idx = df[df['minimum_nights_avg_ntm'] > 70].index
remove_outliers(df, outliers_idx, debug_col='minimum_nights_avg_ntm')
Se aplica arbitrariamente una regla de rango intercuartil (IQR) para excluir valores extremos.
df['bedrooms'].fillna(0, inplace=True)
outliers_idx = get_outliers_iqr(df['bedrooms'], 25)[0]
remove_outliers(df, outliers_idx, debug_col='bedrooms')
Las características que a priori son las más importantes en el problema que se plantea son los precios y las reviews, por la relación directa en los ingresos del anfitrión y en los costes para el huésped.
Las reviews constituyen la manera más cercana de estimar el nivel de ocupación de la vivienda a falta de datos explícitos al respecto como podrían ser las duraciones de las estancias o el número de huéspedes que definen cada estancia.
En los siguientes apartados se analizan diferentes relaciones entre las múltiples características de los alojamientos, siendo especialmente relevantes los precios y las reviews como se comentaba.
El precio price es la variable objetivo del estudio luego es conveniente ver de partida cómo está distribuido.
Entre 20 y 100 euros por noche se concentra la gran mayoría de alojamientos.
fig = px.histogram(df, x="price", nbins=40)
fig.show()
fig = ff.create_distplot([df['price']], ['price'], bin_size=[25])
fig.show()
pandas_profiling.ProfileReport(df)
En los mapas de correlación se pueden obtener algunos indicios interesantes de relaciones entre características a parte de las relaciones evidentes por familias como por ejemplo la familia de los precios o la familia de las reviews.
def print_corr_map(df, height=None):
corrs = df.corr()
fig = go.Figure(
data=go.Heatmap(
z=corrs.values,
x=list(corrs.columns),
y=list(corrs.index),
showscale=True
)
)
if height:
fig.update_layout(height=height)
fig.show()
key_cols = [
'price',
'price_med_occupation_per_accommodate',
'income_med_occupation',
'review_scores_rating',
'reviews_per_month'
]
misc_cols = [
'activity_months',
'accommodates',
'bathrooms',
'bedrooms',
'cancellation_policy',
'cleaning_fee',
'district',
'extra_people',
'first_review',
'guests_included',
'instant_bookable',
'has_license',
'host_response_time',
'latitude',
'longitude',
'maximum_nights_avg_ntm',
'minimum_nights_avg_ntm',
'neighbourhood',
'number_of_reviews',
'number_of_reviews_ltm',
'property_type',
'room_type',
'security_deposit',
*key_cols
]
print_corr_map(df[misc_cols], height=900)
review_cols = [
'activity_months',
'instant_bookable',
'review_scores_accuracy',
'review_scores_cleanliness',
'review_scores_checkin',
'review_scores_communication',
'review_scores_location',
'review_scores_value',
*key_cols
]
print_corr_map(df[review_cols])
La inmensa mayoría de las viviendas presentan la licencia turística.
df_by_license = df.groupby(['has_license'])['has_license'].count().to_frame('has_license_count')
df_by_license.reset_index(inplace=True)
fig32 = go.Figure(
go.Pie(
labels=['no_license', 'has_license'],
values=df_by_license['has_license_count']
)
)
fig32.update_traces(
textfont_size=20,
marker=dict(colors=['Orange', 'SteelBlue'])
)
fig32.update_layout(title='license')
fig32.show()
fig322 = go.Figure()
for val in [0, 1]:
fig322.add_trace(
go.Violin(
x=df['has_license'][df['has_license'] == val],
y=df['reviews_per_month'][df['has_license'] == val],
name=val,
meanline_visible=True,
line_color='Orange' if val == 0 else 'SteelBlue'
)
)
fig322.update_layout(title='reviews_per_month x has_license', showlegend=False)
fig322.show()
Claro dominio de apartamentos. En el precio no hay diferencias notables.
df_by_property_type = df.groupby(['property_type'])['property_type'].count().to_frame('property_type_count')
df_by_property_type.reset_index(inplace=True)
property_types = np.sort(df_by_property_type['property_type'].unique())
fig33 = go.Figure(go.Pie(
labels=df_by_property_type['property_type'],
values=df_by_property_type['property_type_count']
))
fig33.update_layout(title='property_type')
fig33.update_traces(textfont_size=15)
fig33.show()
fig332 = go.Figure()
for pt in property_types:
fig332.add_trace(
go.Violin(
x=df['property_type'][df['property_type'] == pt],
y=df['price_med_occupation_per_accommodate'][df['property_type'] == pt],
points='all',
name=pt,
box_visible=True,
meanline_visible=True
)
)
fig332.update_layout(title='price_med_occupation_per_accommodate')
fig332.show()
El formato más habitual de alquiler vacacional es el piso completo, por encima de la habitación privada en piso compartido. Los precios van en sintonía con ello.
df_by_room_type = df.groupby(['room_type'])['room_type'].count().to_frame('room_type_count')
df_by_room_type.reset_index(inplace=True)
room_types = np.sort(df_by_room_type['room_type'].unique())
fig34 = go.Figure(
go.Pie(
labels=df_by_room_type['room_type'],
values=df_by_room_type['room_type_count']
)
)
fig34.update_traces(textfont_size=20)
fig34.update_layout(title='room_type')
fig34.show()
fig342 = go.Figure()
for rt in room_types:
fig342.add_trace(
go.Violin(
x=df['room_type'][df['room_type'] == rt],
y=df['price_med_occupation_per_accommodate'][df['room_type'] == rt],
points='all',
name=rt,
box_visible=True,
meanline_visible=True
)
)
fig342.update_layout(title='price_med_occupation_per_accommodate')
fig342.show()
Puede haber una cierta retroalimentación entre el número de reviews por mes y la nota en las mismas. Lo normal es que cuantas más y mejores reviews tenga una vivienda, más estancias genere.
px.scatter(
df,
x='review_scores_rating',
y='reviews_per_month',
color='room_type'
).show()
No parece que haya un claro impacto del tipo de cancelación sobre el número de estancias. En general los anfitriones utilizan políticas de cancelación que tienen un mínimo de flexibilidad en el tiempo para no ahuyentar a potenciales huéspedes.
df_by_cancellation_policy = df.groupby(['cancellation_policy'])['cancellation_policy'].count().to_frame('cancellation_policy_count')
df_by_cancellation_policy.reset_index(inplace=True)
cancellation_policy_types = np.sort(df_by_cancellation_policy['cancellation_policy'].unique())
fig36 = go.Figure(go.Pie(
labels=df_by_cancellation_policy['cancellation_policy'],
values=df_by_cancellation_policy['cancellation_policy_count']
))
fig36.update_layout(title='cancellation_policy')
fig36.update_traces(textfont_size=15)
fig36.show()
fig362 = go.Figure()
for cpt in cancellation_policy_types:
fig362.add_trace(
go.Violin(
x=df['cancellation_policy'][df['cancellation_policy'] == cpt],
y=df['reviews_per_month'][df['cancellation_policy'] == cpt],
name=cpt,
box_visible=True,
meanline_visible=True
)
)
fig362.update_layout(title='reviews_per_month', showlegend=False)
fig362.show()
El número mínimo de noches a la hora de reservar un alojamiento podría ser un factor limitante. Simplemente es un indicio porque no se conoce la duración de las estancias pero los alojamientos que menos limitan este aspecto tienen más estancias al mes.
px.scatter(
df,
x='minimum_nights_avg_ntm',
y='reviews_per_month'
).show()
A mejor tasa de respuesta, más estancias. Es importante por tanto la agilidad de los anfitriones a la hora de tratar con los potenciales huéspedes.
df['host_response_time'].fillna('-unk-', inplace=True)
df_by_response_time = df.groupby(['host_response_time'])['host_response_time'].count().to_frame('host_response_time_count')
df_by_response_time.reset_index(inplace=True)
response_time_types = np.sort(df_by_response_time['host_response_time'].unique())
fig38 = go.Figure()
for rtt in response_time_types:
fig38.add_trace(
go.Violin(
x=df['host_response_time'][df['host_response_time'] == rtt],
y=df['reviews_per_month'][df['host_response_time'] == rtt],
name=rtt,
box_visible=True,
meanline_visible=True
)
)
fig38.update_layout(title='reviews_per_month', showlegend=False)
fig38.show()
Una mayor tasa de limpieza se relaciona con viviendas que se alquilan de forma completa. Cuanto más alta es la tasa de limpieza, menos reservas al mes se obtienen posiblemente por tratarse de viviendas exclusivas.
px.scatter(
df,
x='cleaning_fee',
y='reviews_per_month',
color='room_type'
).show()
px.scatter(
df, x='cleaning_fee',
y='income_med_occupation',
color='room_type'
).show()
El centro tiene una concentración mucho más alta de alojamientos.
df_by_nb = df.groupby(['neighbourhood'])['neighbourhood'].size().to_frame('count')
df_by_nb.reset_index(inplace=True)
fig310 = go.Figure(go.Choroplethmapbox(
geojson=city_nb,
locations=df_by_nb['neighbourhood'],
z=df_by_nb['count'],
colorscale='Blues',
marker_opacity=0.5,
marker_line_width=0.5
))
fig310.update_layout(
mapbox_style='carto-positron',
mapbox_zoom=11,
mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
margin={"r":0,"t":0,"l":0,"b":0}
)
fig310.show()
En general son los barrios del centro los que presentan precios más altos.
fig311 = go.Figure(
go.Scattermapbox(
lon=df['longitude'],
lat=df['latitude'],
mode='markers',
marker_color=df['price_med_occupation_per_accommodate'],
text=df['price_med_occupation_per_accommodate'],
marker=dict(
opacity=0.5,
colorscale='Blues',
cmin=df['price'].min(),
cmax=df['price'].max()
)
)
)
fig311.update_layout(
mapbox_style='carto-positron',
mapbox_zoom=11,
mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
margin={"r":0,"t":0,"l":0,"b":0}
)
fig311.show()
Las notas de las reviews no tienen una relación clara con el precio pagado por el huésped (ya sea alto o bajo).
px.scatter(
df,
x='review_scores_rating',
y='price_med_occupation_per_accommodate'
).show()
Los alojamientos con precios por noche en el rango entre 70 y 140 euros son los que consiguen más estancias.
px.scatter(
df,
x='price_med_occupation_per_accommodate',
y='reviews_per_month',
marginal_x='box',
trendline='ols'
).show()
La valoración de localización es un aspecto muy abierto e interesante. Se puede considerar una localización en función de si está bien conectada con el resto de la ciudad, o de si está próxima a puntos de interés, o de si está bien ubicado con respecto a cualquier intención concreta ya sea de tipo turístico, laboral, familiar, etc. que cualquier huésped puede tener.
En el mapa se muestra que efectivamente los barrios del centro tienen una gran calificación en este aspecto pero tampoco hay diferencias muy grandes con barrios un poco más lejanos al centro.
df_by_nb = df.groupby(['neighbourhood'])['review_scores_location'].mean().to_frame('review_scores_location_avg')
df_by_nb.reset_index(inplace=True)
fig3142 = go.Figure(go.Choroplethmapbox(
geojson=city_nb,
locations=df_by_nb['neighbourhood'],
z=df_by_nb['review_scores_location_avg'],
colorscale='Blues',
marker_opacity=0.5,
marker_line_width=0.5
))
fig3142.update_layout(
mapbox_style='carto-positron',
mapbox_zoom=11,
mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
margin={"r":0,"t":0,"l":0,"b":0}
)
fig3142.show()
Entre 60 y 150 euros por noche es el rango más habitual de precio que paga un huésped por noche por una estancia de duración media.
fig = px.histogram(df, x='price_med_occupation_per_accommodate', nbins=40)
fig.show()
fig = go.Figure()
fig.add_trace(go.Histogram(
x=df['price_med_occupation_per_accommodate'],
name='price_med_occupation_per_accommodate',
marker_color='#EB89B5'))
fig.add_trace(go.Histogram(
x=df['price'],
name='price',
marker_color='#330C73'))
"""
fig.add_trace(go.Histogram(
x=df['income_med_occupation'],
name='income_med_occupation',
marker_color='green'))
"""
fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()
Dominan con claridad los alojamientos con pocas estancias, en concreto con un baño y un dormitorio.
plt.figure(figsize=(16, 8))
plt.hist(df['bedrooms'])
plt.gca().set(title='BEDROOMS', ylabel='COUNT');
df['bathrooms'].fillna(0, inplace=True)
plt.figure(figsize=(16, 8))
plt.hist(df['bathrooms'])
plt.gca().set(title='BATHROOMS', ylabel='COUNT');
df_bb = df.groupby(['bedrooms', 'bathrooms'])[['bedrooms', 'bathrooms']].size().to_frame('cnt')
df_bb.reset_index(inplace=True)
fig316 = px.scatter(df_bb, x='bedrooms', y='bathrooms', size='cnt')
fig316.show()
df_by_price_per_bed = df.groupby(['district', 'accommodates'])['income_med_occupation'].mean().to_frame('income_med_occupation_avg')
df_by_price_per_bed.reset_index(inplace=True)
px.line(
df_by_price_per_bed,
x='accommodates',
y='income_med_occupation_avg',
color='district'
).show()
Estas distribuciones refuerzan el análisis del apartado anterior.
districts = np.sort(df['district'].unique())[::-1]
fig318 = go.Figure()
for d in districts:
fig318.add_trace(go.Violin(
x=df[df['district'] == d]['income_med_occupation'].values,
name=' ' + d + ' '
))
fig318.update_traces(
orientation='h',
side='positive',
width=3,
points=False
)
fig318.update_layout(
height=900,
xaxis_showgrid=False,
xaxis_zeroline=False,
showlegend=False
)
fig318.show()
px.scatter(
df,
x='accommodates',
y='income_med_occupation',
trendline='ols',
color='district'
).show()
En general son los barrios más céntricos los que mayor precio por huésped tienen aunque hay diversas excepciones.
Los barrios periféricos en general presentan precios más bajos que el resto.
df_by_nb = df.groupby(['neighbourhood'])['price_med_occupation_per_accommodate'].mean().to_frame('price_med_occupation_per_accommodate_avg')
df_by_nb.reset_index(inplace=True)
fig320 = go.Figure(go.Choroplethmapbox(
geojson=city_nb,
locations=df_by_nb['neighbourhood'],
z=df_by_nb['price_med_occupation_per_accommodate_avg'],
colorscale='Blues',
marker_opacity=0.5,
marker_line_width=0.5
))
fig320.update_layout(
mapbox_style='carto-positron',
mapbox_zoom=11,
mapbox_center={'lat':df['latitude'].mean(), 'lon':df['longitude'].mean()},
margin={"r":0,"t":0,"l":0,"b":0}
)
fig320.show()
A lo largo del estudio se han presentado diferentes relaciones entre características donde participan el precio medio por noche y huésped y los ingresos por estancia media. Ambas variables están basadas en la característica precio y por ello tienen una clara relación con ella así que es probable que en la fase de modelado se descarte el uso de alguna de ellas.
fig = go.Figure()
fig.add_trace(go.Histogram(
x=df['price_med_occupation_per_accommodate'],
name='price_med_occupation_per_accommodate',
marker_color='#EB89B5'))
fig.add_trace(go.Histogram(
x=df['income_med_occupation'],
name='income_med_occupation',
marker_color='blue'))
fig.update_layout(barmode='stack')
fig.update_traces(opacity=0.75)
fig.show()
fig323 = go.Figure()
for x in ['price_med_occupation_per_accommodate', 'income_med_occupation']:
fig323.add_trace(go.Scatter(
x=df['price'],
y=df[x],
mode='markers',
name=x,
marker=dict(color=('blue' if x != 'price_med_occupation_per_accommodate' else '#EB89B5'))
))
fig323.update_traces(marker=dict(opacity=0.5))
fig323.show()
En las correlaciones simplemente se observa más afinidad entre objetos que normalmente se ubican en una cocina como el frigorífico, el microondas, la cafetera o la vajilla.
No existe un impacto claro entre alguna comodidad concreta y las reviews en número o nota de las estancias.
Curiosamente el aire acondicionado es una de las comodidades que menos destaca, bien es cierto que es algo que tiene importancia sobre todo de forma estacional y según para qué tipo de huéspedes.
reviews_cols = [
'reviews_per_month',
'review_scores_rating'
]
amenities_cols = [
'has_air_conditioning',
'has_bed_linens',
'has_coffee_maker',
'has_cooking_basics',
'has_dishes_and_silverware',
'has_elevator',
'has_essentials',
'has_family/kid_friendly',
'has_first_aid_kit',
'has_hair_dryer',
'has_hangers',
'has_heating',
'has_hot_water',
'has_iron',
'has_kitchen',
'has_laptop_friendly_workspace',
'has_microwave',
'has_no_stairs_or_steps_to_enter',
'has_oven',
'has_refrigerator',
'has_shampoo',
'has_stove',
'has_tv',
'has_washer',
'has_wifi'
]
print_corr_map(df[[*amenities_cols, *reviews_cols]], height=900)
"""
for am in amenities_cols:
fig324 = go.Figure()
fig324 = make_subplots(
rows=1,
cols=2,
subplot_titles=('reviews_per_month', 'review_scores_rating')
)
for var in reviews_cols:
for val in [0, 1]:
fig324.add_trace(go.Violin(
x=df[am][df[am] == val],
y=df[var][df[am] == val],
name=val,
meanline_visible=True,
line_color='Orange' if val == 0 else 'SteelBlue'
),
row=1,
col=1 if var == 'reviews_per_month' else 2
)
fig324.update_layout(title=am, showlegend=False)
fig324.show()
"""
df.to_csv(filename_out, index=False)